#region Copyright (c) 11/25/2011, Olaf Kober <amarok.projects@gmail.com>
//================================================================================
//	Permission is hereby granted, free of charge, to any person obtaining a copy
//	of this software and associated documentation files (the "Software"), to deal
//	in the Software without restriction, including without limitation the rights
//	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//	copies of the Software, and to permit persons to whom the Software is
//	furnished to do so, subject to the following conditions:
//
//	The above copyright notice and this permission notice shall be included in
//	all copies or substantial portions of the Software.
//
//	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//	THE SOFTWARE.
//================================================================================
#endregion

using System;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Windows.Controls;


namespace Amarok.Presentation
{
	/// <summary>
	/// This type provides a base class for view model implementations. This base class provides rich support 
	/// for managing validation data errors in addition to property change notifications. If you just need 
	/// support property change notifications, you should derive from base class <see cref="ViewModel"/> instead.
	/// </summary>
	public abstract class ValidatingViewModel : ViewModel,
		IDataErrorInfo
	{
		// data
		private readonly StringDictionary<String> mPropertyErrors;
		private String mObjectError;


		#region ++ IDataErrorInfo Interface ++

		/// <summary>
		/// Gets an error message indicating what is wrong with this object.
		/// </summary>
		/// 
		/// <value>
		/// An error message indicating what is wrong with this object. The default is an empty string.</value>
		public String Error
		{
			get
			{
				return mObjectError ?? String.Empty;
			}
		}

		/// <summary>
		/// Gets the error message for the property with the given name.
		/// </summary>
		/// 
		/// <param name="propertyName">
		/// The name of the property whose error message to return.</param>
		/// 
		/// <returns>
		/// The error message for the property. The default is an empty string.</returns>
		public String this[String propertyName]
		{
			get
			{
				if (propertyName == null || propertyName.Length == 0)
					return String.Empty;

				if (mPropertyErrors.Count == 0)
					return String.Empty;

				return mPropertyErrors.GetValueOrDefault(propertyName, String.Empty);
			}
		}

		#endregion

		#region ++ Public Interface (Error Management) ++

		/// <summary>
		/// Gets a boolean value indicating whether this object has any validation data error.
		/// </summary>
		public Boolean HasErrors
		{
			get
			{
				if (mObjectError != null && mObjectError.Length > 0)
					return true;

				return mPropertyErrors.Count > 0;
			}
		}


		/// <summary>
		/// Gets a boolean value indicating whether the specified property has an error state.
		/// </summary>
		/// 
		/// <param name="propertyExpression">
		/// A lambda expression specifying the property.</param>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// The lambda expression is not a member access expression.</exception>
		/// <exception cref="ArgumentException">
		/// The lambda expression does not refer to a property.</exception>
		/// <exception cref="ArgumentException">
		/// The lambda expression does not refer to an instance member.</exception>
		public Boolean HasError<T>(Expression<Func<T>> propertyExpression)
		{
			Verify.NotNull(propertyExpression, "propertyExpression");

			if (mPropertyErrors.Count == 0)
				return false;

			var propertyName = Helper.GetPropertyName(
				propertyExpression);

			return mPropertyErrors.ContainsKey(propertyName);
		}

		/// <summary>
		/// Gets a boolean value indicating whether the specified property has an error state.
		/// </summary>
		/// 
		/// <param name="propertyName">
		/// The name of the property.</param>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// An empty string was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// The type does not contain a public instance member property with specified name.</exception>
		public Boolean HasError(String propertyName)
		{
			base.VerifyPropertyName(propertyName);

			if (mPropertyErrors.Count == 0)
				return false;

			return mPropertyErrors.ContainsKey(propertyName);
		}


		/// <summary>
		/// Sets the error state of this object. Specifying an empty string will clear the error state 
		/// for this object.
		/// </summary>
		/// 
		/// <param name="message">
		/// An error message, which may contain placeholders.</param>
		/// <param name="arguments">
		/// An optional list of arguments to format into placeholders contained in the error message.</param>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		public void SetObjectError(String message, params Object[] arguments)
		{
			Verify.NotNull(message, "message");

			var formattedMessage = String.Format(CultureInfo.CurrentCulture,
				message, arguments);

			mObjectError = formattedMessage;

			base.RaisePropertyChanged(() => this.Error);
			base.RaisePropertyChanged(() => this.HasErrors);
		}

		/// <summary>
		/// Sets an error with the supplied error message for the specified property. Specifying an empty string 
		/// will clear the error state for the specified property.
		/// </summary>
		/// 
		/// <param name="propertyExpression">
		/// A lambda expression specifying the property for which to set an error.</param>
		/// <param name="message">
		/// An error message, which may contain placeholders.</param>
		/// <param name="arguments">
		/// An optional list of arguments to format into placeholders contained in the error message.</param>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// The lambda expression is not a member access expression.</exception>
		/// <exception cref="ArgumentException">
		/// The lambda expression does not refer to a property.</exception>
		/// <exception cref="ArgumentException">
		/// The lambda expression does not refer to an instance member.</exception>
		public void SetError<T>(Expression<Func<T>> propertyExpression, String message, params Object[] arguments)
		{
			Verify.NotNull(propertyExpression, "propertyExpression");
			Verify.NotNull(message, "message");

			var propertyName = Helper.GetPropertyName(
				propertyExpression);

			var formattedMessage = String.Format(CultureInfo.CurrentCulture,
				message, arguments);

			var hasChanged = true;

			if (formattedMessage.Length == 0)
				hasChanged = mPropertyErrors.Remove(propertyName);
			else
				mPropertyErrors[propertyName] = formattedMessage;

			if (hasChanged)
			{
				base.RaisePropertyChanged(propertyName);
				base.RaisePropertyChanged(() => this.HasErrors);
			}
		}

		/// <summary>
		/// Sets an error with the supplied error message for the specified property. Specifying an empty string 
		/// will clear the error state for the specified property.
		/// </summary>
		/// 
		/// <param name="propertyName">
		/// The name of the property for which to set an error.</param>
		/// <param name="message">
		/// An error message, which may contain placeholders.</param>
		/// <param name="arguments">
		/// An optional list of arguments to format into placeholders contained in the error message.</param>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// An empty string was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// The type does not contain a public instance member property with specified name.</exception>
		public void SetError(String propertyName, String message, params Object[] arguments)
		{
			base.VerifyPropertyName(propertyName);
			Verify.NotNull(message, "message");

			var formattedMessage = String.Format(CultureInfo.CurrentCulture,
				message, arguments);

			var hasChanged = true;

			if (formattedMessage.Length == 0)
				hasChanged = mPropertyErrors.Remove(propertyName);
			else
				mPropertyErrors[propertyName] = formattedMessage;

			if (hasChanged)
			{
				base.RaisePropertyChanged(propertyName);
				base.RaisePropertyChanged(() => this.HasErrors);
			}
		}


		/// <summary>
		/// Clears all validation data errors.
		/// </summary>
		public void ClearErrors()
		{
			var errorsCleared = 0;

			if (mObjectError != null && mObjectError.Length > 0)
			{
				mObjectError = String.Empty;
				errorsCleared++;

				base.RaisePropertyChanged(() => this.Error);
			}

			if (mPropertyErrors.Count > 0)
			{
				var keys = mPropertyErrors.Keys.ToArray();
				errorsCleared += keys.Length;

				mPropertyErrors.Clear();

				for (Int32 i = 0; i < keys.Length; i++)
					base.RaisePropertyChanged(keys[i]);
			}

			if (errorsCleared > 0)
				base.RaisePropertyChanged(() => this.HasErrors);
		}


		/// <summary>
		/// Clears the validation data error on this object.
		/// </summary>
		public void ClearObjectError()
		{
			var errorsCleared = 0;

			if (mObjectError != null && mObjectError.Length > 0)
			{
				mObjectError = null;
				errorsCleared++;
			}

			if (errorsCleared > 0)
			{
				base.RaisePropertyChanged(() => this.Error);
				base.RaisePropertyChanged(() => this.HasErrors);
			}
		}

		/// <summary>
		/// Clears the validation data error on the specified property.
		/// </summary>
		/// 
		/// <param name="propertyExpression">
		/// A lambda expression specifying the property for which to clear an error.</param>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// The lambda expression is not a member access expression.</exception>
		/// <exception cref="ArgumentException">
		/// The lambda expression does not refer to a property.</exception>
		/// <exception cref="ArgumentException">
		/// The lambda expression does not refer to an instance member.</exception>
		public void ClearError<T>(Expression<Func<T>> propertyExpression)
		{
			Verify.NotNull(propertyExpression, "propertyExpression");

			if (mPropertyErrors.Count == 0)
				return;

			var propertyName = Helper.GetPropertyName(
				propertyExpression);

			if (mPropertyErrors.Remove(propertyName))
			{
				base.RaisePropertyChanged(propertyName);
				base.RaisePropertyChanged(() => this.HasErrors);
			}
		}

		/// <summary>
		/// Clears the error on the specified property.
		/// </summary>
		/// 
		/// <param name="propertyName">
		/// The name of the property for which to clear an error.</param>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// An empty string was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// The type does not contain a public instance member property with specified name.</exception>
		public void ClearError(String propertyName)
		{
			base.VerifyPropertyName(propertyName);

			if (mPropertyErrors.Count == 0)
				return;

			if (mPropertyErrors.Remove(propertyName))
			{
				base.RaisePropertyChanged(propertyName);
				base.RaisePropertyChanged(() => this.HasErrors);
			}
		}

		#endregion

		#region ++ Public Interface (Validation) ++

		/// <summary>
		/// Runs the supplied validation rule with the given value. In case that the validation rule succeeds, 
		/// any validation data error on the specified property is cleared. Otherwise, if the validation rule 
		/// fails, a validation data error with the error messages returned by the validation rule is set on the 
		/// specified property.
		/// </summary>
		/// 
		/// <param name="validationRule">
		/// The validation rule to run with the given value.</param>
		/// <param name="value">
		/// The value with that the validation rule should be run.</param>
		/// <param name="propertyExpression">
		/// A lambda expression specifying the property for which validation errors should be cleared or set.</param>
		/// 
		/// <returns>
		/// The result of the validation rule that was run.</returns>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// The lambda expression is not a member access expression.</exception>
		/// <exception cref="ArgumentException">
		/// The lambda expression does not refer to a property.</exception>
		/// <exception cref="ArgumentException">
		/// The lambda expression does not refer to an instance member.</exception>
		public ValidationResult Validate<T>(ValidationRule validationRule, Object value,
			Expression<Func<T>> propertyExpression)
		{
			Verify.NotNull(validationRule, "validationRule");
			Verify.NotNull(propertyExpression, "propertyExpression");

			var result = validationRule.Validate(value, CultureInfo.CurrentCulture);

			if (result.IsValid)
				this.ClearError(propertyExpression);
			else
				this.SetError(propertyExpression, result.ErrorContent.ToString());

			return result;
		}

		/// <summary>
		/// Runs the supplied validation rule with the given value. In case that the validation rule succeeds, 
		/// any validation data error on the specified property is cleared. Otherwise, if the validation rule 
		/// fails, a validation data error with the error messages returned by the validation rule is set on the 
		/// specified property.
		/// </summary>
		/// 
		/// <param name="validationRule">
		/// The validation rule to run with the given value.</param>
		/// <param name="value">
		/// The value with that the validation rule should be run.</param>
		/// <param name="propertyName">
		/// The name of the property for which validation errors should be cleared or set.</param>
		/// 
		/// <returns>
		/// The result of the validation rule that was run.</returns>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// An empty string was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// The type does not contain a public instance member property with specified name.</exception>
		public ValidationResult Validate(ValidationRule validationRule, Object value, String propertyName)
		{
			Verify.NotNull(validationRule, "validationRule");
			base.VerifyPropertyName(propertyName);

			var result = validationRule.Validate(value, CultureInfo.CurrentCulture);

			if (result.IsValid)
				this.ClearError(propertyName);
			else
				this.SetError(propertyName, result.ErrorContent.ToString());

			return result;
		}

		/// <summary>
		/// Runs the supplied validation rule with the given value. In case that the validation rule succeeds, 
		/// any validation data error on the specified property is cleared. Otherwise, if the validation rule 
		/// fails, a validation data error with the error messages returned by the validation rule is set on the 
		/// specified property.
		/// </summary>
		/// 
		/// <param name="validationRule">
		/// The validation rule to run with the given value.</param>
		/// <param name="value">
		/// The value with that the validation rule should be run.</param>
		/// <param name="propertyExpression">
		/// A lambda expression specifying the property for which validation errors should be cleared or set.</param>
		/// 
		/// <returns>
		/// The result of the validation rule that was run.</returns>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// The lambda expression is not a member access expression.</exception>
		/// <exception cref="ArgumentException">
		/// The lambda expression does not refer to a property.</exception>
		/// <exception cref="ArgumentException">
		/// The lambda expression does not refer to an instance member.</exception>
		public ValidationResult Validate<V, T>(ValidationRule<V> validationRule, V value,
			Expression<Func<T>> propertyExpression)
		{
			return this.Validate<T>((ValidationRule)validationRule, value, propertyExpression);
		}

		/// <summary>
		/// Runs the supplied validation rule with the given value. In case that the validation rule succeeds, 
		/// any validation data error on the specified property is cleared. Otherwise, if the validation rule 
		/// fails, a validation data error with the error messages returned by the validation rule is set on the 
		/// specified property.
		/// </summary>
		/// 
		/// <param name="validationRule">
		/// The validation rule to run with the given value.</param>
		/// <param name="value">
		/// The value with that the validation rule should be run.</param>
		/// <param name="propertyName">
		/// The name of the property for which validation errors should be cleared or set.</param>
		/// 
		/// <returns>
		/// The result of the validation rule that was run.</returns>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// An empty string was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// The type does not contain a public instance member property with specified name.</exception>
		public ValidationResult Validate<V>(ValidationRule<V> validationRule, V value, String propertyName)
		{
			return this.Validate((ValidationRule)validationRule, value, propertyName);
		}

		#endregion

		#region ## Protected Interface ##

		/// <summary>
		/// Initializes a new instance.
		/// </summary>
		protected ValidatingViewModel()
		{
			mPropertyErrors = new StringDictionary<String>(StringComparer.Ordinal);
		}

		#endregion

	}
}
